﻿#region header

//  ***************************************************************************
// *                                LICENSE
//  ***************************************************************************
//  * "Haplous" .NET Simple 2D Game Framework
//  * 
//  * Copyright © Jonathan Evans 2014-2014
//  * 
//  * This program is free software; you can redistribute it and/or
//  * modify it under the terms of the GNU General Public License
//  * as published by the Free Software Foundation; either version 2
//  * of the License, or (at your option) any later version.
//  * 
//  * This program is distributed in the hope that it will be useful,
//  * but WITHOUT ANY WARRANTY; without even the implied warranty of
//  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  * GNU General Public License for more details.
//  * 
//  * You should have received a copy of the GNU General Public License
//  * along with this program; if not, write to the Free Software
//  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
//  * 
//  ***************************************************************************
//  * 
//  * Contact: opencollar@googlemail.com
//  * 
//  * Website: http://haplous.codeplex.com/
//  *
//  * NuGet:   http://www.nuget.org/packages/haplous/
//  *
//  * File:    Haplous/Haplous/DisposableNotifyObject.cs
//  *
//  ***************************************************************************

#endregion

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading;

using Haplous.Correctness;
using Haplous.Properties;

using JetBrains.Annotations;

namespace Haplous.Core
{
    /// <summary>
    ///     A base class for objects that implement the <see cref="IDisposable" /> and <see cref="INotifyPropertyChanged" />
    ///     interfaces.
    /// </summary>
    [DebuggerDisplay("DisposableNotifyObject: IsDisposed={IsDispose}")]
    public abstract class DisposableNotifyObject : Disposable, INotifyPropertyChanged
    {
        #region INotifyPropertyChanged

        /// <summary>
        ///     Occurs when a property value changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        ///     Called when a property has changed and the <see cref="PropertyChanged" /> event should be raised.
        /// </summary>
        /// <param name="propertyName">The name of the property that has changed.</param>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged([NotNull] string propertyName)
        {
            if(string.IsNullOrWhiteSpace(propertyName))
                throw new ArgumentOutOfRangeException("propertyName", propertyName, Resources.Error_PropertyNameMustAlwaysBeSupplied);

            var handler = PropertyChanged;
            if(handler == null)
                return;

            var callbacks = handler.GetInvocationList();
            if(callbacks.Length <= 0)
                return;

            var args = new PropertyChangedEventArgs(propertyName);

            foreach(var callback in callbacks)
            {
                Debug.Assert(callback != null, "callback != null");

                try
                {
                    callback.DynamicInvoke(this, args);
                }
                catch(Exception ex)
                {
                    ErrorManager.OnUnhandledException(ex);
                }
            }
        }

        /// <summary>
        ///     Called when a new value is assigned to a property, this method updates the backing field supplied and raises the
        ///     <see cref="INotifyPropertyChanged.PropertyChanged" /> event.
        /// </summary>
        /// <typeparam name="T">The type of the property.</typeparam>
        /// <param name="propertyName">The name of the property that has changed.</param>
        /// <param name="field">The field used to hold the current value.</param>
        /// <param name="value">The new value to assign.</param>
        /// <returns>
        ///     <see langword="true" /> if a new value has been assigned to the backing field, <see langword="false" /> if the
        ///     value assigned was no different to the existing value.
        /// </returns>
        /// <remarks>
        ///     This method checks to see if the property has changed, if it has then the new value is assigned
        ///     to the field given and the <see cref="PropertyChanged" /> event is raised.  Otherwise no change is made.
        /// </remarks>
        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "1#")]
        [NotifyPropertyChangedInvocator]
        protected virtual bool OnPropertyChanged<T>([NotNull] string propertyName, ref T field, T value)
        {
            if(Equals(field, value))
                return false;

            field = value;

            OnPropertyChanged(propertyName);

            return true;
        }

        /// <summary>
        ///     Called when a new value is assigned to a property, this method updates the backing field using the lock supplied
        ///     and raises the <see cref="INotifyPropertyChanged.PropertyChanged" /> event.
        /// </summary>
        /// <typeparam name="T">The type of the property.</typeparam>
        /// <param name="propertyName">The name of the property that has changed.</param>
        /// <param name="field">The field used to hold the current value.</param>
        /// <param name="value">The new value to assign.</param>
        /// <param name="slimLock">
        ///     A lock used to control concurrent access to the field being updated.  An upgradeable lock is
        ///     taken whilst checking if the value has changed.  If it has changed then a write lock is taken to make the change.
        ///     All locks are guaranteed to be released.
        /// </param>
        /// <returns>
        ///     <see langword="true" /> if a new value has been assigned to the backing field, <see langword="false" /> if the
        ///     value assigned was no different to the existing value.
        /// </returns>
        /// <remarks>
        ///     This method checks to see if the property has changed, if it has then the new value is assigned
        ///     to the field given and the <see cref="PropertyChanged" /> event is raised.  Otherwise no change is made.
        /// </remarks>
        [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", MessageId = "1#")]
        [NotifyPropertyChangedInvocator]
        protected virtual bool OnPropertyChanged<T>([NotNull] string propertyName, ref T field, T value, [NotNull] ReaderWriterLockSlim slimLock)
        {
            slimLock.ValidateArgument("slimLock");

            slimLock.EnterUpgradeableReadLock();
            try
            {
                if(Equals(field, value))
                    return false;
                slimLock.EnterWriteLock();
                try
                {
                    field = value;
                }
                finally
                {
                    slimLock.ExitWriteLock();
                }
            }
            finally
            {
                slimLock.ExitUpgradeableReadLock();
            }
            OnPropertyChanged(propertyName);

            return true;
        }

        #endregion

        // The contents of the following region must be kept in sync with the implementation in NotifyObject.cs.
    }
}